﻿using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Text;
using System.Threading;
using System.Xml;
using Common.Utilities;
using Common.Utilities.Dispatcher;
using PdfLib;

namespace Workflows.Components.TextMining.EnumerationObjects
{
    /// <summary>
    /// 
    /// </summary>
    public class PdfScanWorker:IWorker
    {
        #region fields
        private Thread _WorkerThread;
        private Guid _JobTicketID;
        private PdfScanInstruction _Instruction;
        private PdfScanOutput _Output;
        private RunContext _Context;
        private string _WorkflowName;
        private string _ComponentName;

        /// <summary>
        /// 
        /// </summary>
        public string ComponentName
        {
            get { return _ComponentName; }
        }

        /// <summary>
        /// 
        /// </summary>
        public string WorkflowName
        {
            get { return _WorkflowName; }
        }

        private object _SyncObj;
        private StreamWriter _LogWriter;
        private StreamWriter _ErrorWriter;
        private ManualResetEvent[] _ManualResets;
        private string[] _CurrentWorkloads;
        private Dictionary<string, int> _FilePathIDMappings;
        private XmlTextWriter _OutputWriter;

        #endregion 

        #region IWorker Members

        /// <summary>
        /// handler to worker thread
        /// </summary>
        public Thread WorkerThread
        {
            get { return _WorkerThread; }
        }

        /// <summary>
        /// allow instruction to be changed at runtime
        /// </summary>
        public InstructionBase Instruction
        {
            get { return _Instruction; }
        }

        /// <summary>
        /// output
        /// </summary>
        public ResultBase Output
        {
            get { return _Output; }
        }

        /// <summary>
        /// allow worker to be created based on component type
        /// </summary>
        public Type ComponentType
        {
            get { return typeof(PdfScanComponent); }
        }

        /// <summary>
        /// runtime context retrieved from appl domain
        /// </summary>
        public RunContext Context
        {
            get { return this._Context; }
            set { this._Context=value; }
        }

        /// <summary>
        /// this must be called before actual work
        /// </summary>
        /// <param name="ticketGuid"></param>
        /// <param name="request"></param>
        public void SetInstruction(Guid ticketGuid, InstructionBase request, string workflowName, string componentName)
        {
            this._JobTicketID = ticketGuid;
            this._Instruction = (PdfScanInstruction) request;
            this._WorkflowName = workflowName;
            this._ComponentName = componentName;
        }

        /// <summary>
        /// this is called within subflow, where runtime context is also initialized
        /// </summary>
        /// <param name="context"></param>
        /// <param name="ticketGuid"></param>
        /// <param name="request"></param>
        public void SetInstruction(RunContext context, Guid ticketGuid, InstructionBase request, string workflowName, string componentName)
        {
            this._JobTicketID = ticketGuid;
            this._Instruction = (PdfScanInstruction)request;
            this._Context = context;
            this._WorkflowName = workflowName;
            this._ComponentName = componentName;
        }

        /// <summary>
        /// this is called synchronously, used in testing mode
        /// </summary>
        public void DoWork()
        {
            if(this.WorkerStartedEvent !=null)
            {
                this.WorkerStartedEvent(this._JobTicketID, this, this.GetType(), this._Instruction);
            }
            this._WorkerThread = Thread.CurrentThread;
            this.ScanPdfFiles();
        }


        /// <summary>
        /// this is called by job dispatcher
        /// </summary>
        public void DoWorkAsync()
        {
            if (this.WorkerStartedEvent != null)
            {
                this.WorkerStartedEvent(this._JobTicketID, this, this.GetType(), this._Instruction);
            }
            this._WorkerThread = new Thread(new ThreadStart(this.ScanPdfFiles));
            this._WorkerThread.Start();
        }

        /// <summary>
        /// this is called by job monitor/manager
        /// </summary>
        public void CancelWork()
        {
            if(this._WorkerThread !=null && this._WorkerThread.IsAlive)
            {
                this._WorkerThread.Abort();
                if(this.WorkerCancelledEvent !=null)
                {
                    this.WorkerCancelledEvent(this._JobTicketID, this.GetType(), this._Instruction);
                }
            }
        }

        public event WorkerStarted WorkerStartedEvent;
        public event WorkerFinished WorkerFinishedEvent;
        public event WorkerHasError WorkerErredEvent;
        public event WorkerCancelled WorkerCancelledEvent;

        public event WorkerStatusChanged WorkerStatusChangedEvent;

        #endregion

        #region work
        private void ScanPdfFiles()
        {
            _SyncObj=new object();
            this._ManualResets= new ManualResetEvent[this._Instruction.ThreadPoolSize];
            this._CurrentWorkloads=new string[this._Instruction.ThreadPoolSize];

            try
            {
                string appFolderPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
                appFolderPath = Path.Combine(appFolderPath, "Workflow data");
                if (!Directory.Exists(appFolderPath))
                {
                    Directory.CreateDirectory(appFolderPath);
                }
                // init 
                if (string.IsNullOrEmpty(this._Instruction.LogFilePath))
                {
                    this._Instruction.LogFilePath = Path.Combine(appFolderPath, "PdfScan.log");
                }
                if (File.Exists(this._Instruction.LogFilePath))
                {
                    File.Delete(this._Instruction.LogFilePath);
                }
                this._LogWriter = new StreamWriter(this._Instruction.LogFilePath);
                if (string.IsNullOrEmpty(this._Instruction.ErrorFilePath))
                {
                    this._Instruction.ErrorFilePath = Path.Combine(appFolderPath, "PdfScan.err");
                }
                if (File.Exists(this._Instruction.ErrorFilePath))
                {
                    File.Delete(this._Instruction.ErrorFilePath);
                }
                this._ErrorWriter = new StreamWriter(this._Instruction.ErrorFilePath);

                // xml writer 
                if (File.Exists(this._Instruction.ResultFilePath))
                {
                    File.Delete(this._Instruction.ResultFilePath);
                }
                this._OutputWriter = new XmlTextWriter(this._Instruction.ResultFilePath, Encoding.UTF8);
                this._OutputWriter.WriteStartElement("Files");

                this._Output =
                    new PdfScanOutput(
                        this._Instruction.OwnerComponentID,
                        this._JobTicketID,
                        new List<string>(),
                        this._Instruction.ResultFilePath);
                this._FilePathIDMappings =
                    this.ReadFileList(this._Instruction.FileListInputXmlFilePath);
                if (_FilePathIDMappings == null || _FilePathIDMappings.Count == 0)
                    throw new Exception("Unable to retrieve file list");
                string[] inputFilePaths = new string[_FilePathIDMappings.Count];
                _FilePathIDMappings.Keys.CopyTo(inputFilePaths, 0);
                // browse and loop
                //DirectoryInfo dirInfo=new DirectoryInfo(this._Instruction.FolderPath);
                //FileInfo[] pdfFiles = dirInfo.GetFiles("*.pdf", SearchOption.AllDirectories);
                //if (pdfFiles != null && pdfFiles.Length > 0)

                int i = 0;
                while (i < inputFilePaths.Length)
                {
                    List<string> filePaths = new List<string>();
                    for (int workerID = 0; workerID < this._Instruction.ThreadPoolSize; workerID++)
                    {
                        if (i < inputFilePaths.Length)
                        {
                            filePaths.Add(inputFilePaths[i]);
                            i++;
                        }
                        else
                            break;
                    }
                    this._CurrentWorkloads = filePaths.ToArray();
                    this._ManualResets = new ManualResetEvent[filePaths.Count];
                    for (int k = 0; k < filePaths.Count; k++)
                    {
                        this._ManualResets[k] = new ManualResetEvent(false);
                        ThreadPool.QueueUserWorkItem(new WaitCallback(ScanPdfFile), k);
                    }
                    WaitHandle.WaitAll(this._ManualResets);

                    int fileScanned = i;
                    if (this.WorkerStatusChangedEvent != null && fileScanned%10 == 0)
                    {
                        int percent = fileScanned*100/
                                      inputFilePaths.Length;
                        string msg = string.Format("Scanned {0} of {1}: {2}...", fileScanned, inputFilePaths.Length,
                                                   inputFilePaths[i - 1]);
                        this.WorkerStatusChangedEvent(this._JobTicketID, this.GetType(), this._Instruction, this.WorkflowName, this.ComponentName, msg,
                                                      percent);
                    }
                }

                this._OutputWriter.WriteEndElement();

                if (this.WorkerFinishedEvent != null)
                {
                    this.WorkerFinishedEvent(this._JobTicketID, this.GetType(), this._Instruction, this._Output);
                }

                
            }
            catch (Exception ex)
            {
                if (this.WorkerErredEvent != null)
                {
                    this.WorkerErredEvent(this._JobTicketID, this.GetType(), this._Instruction, ex.ToString(),true);
                }
            }
            finally
            {
                this._LogWriter.Flush();
                this._LogWriter.Close();

                this._ErrorWriter.Flush();
                this._ErrorWriter.Close();

                this._OutputWriter.Flush();
                this._OutputWriter.Close();
            }
        }

        private void ScanPdfFile(object pObject)
        {
            int idx = (int) pObject;
            string filePath = this._CurrentWorkloads[idx];
            string fileName = Path.GetFileName(filePath);
            try
            {
                // fileScanned++;
                PdfScanOutput.PdfScanResult scanResult = new PdfScanOutput.PdfScanResult();
                int pageCount = PdfPropertyReader.GetPageCount(filePath);
                List<int> pageNumbers = new List<int>();
                for (int pageNum = 1; pageNum <= pageCount; pageNum++)
                {
                    try
                    {
                        double matchScore = 0;
                        RectangleF rect =
                            PdfPatternSearcher.FoundPattern(
                                filePath, pageNum,
                                this._Instruction.Pattern,
                                false, this._Instruction.MatchScoreThreshold,
                                ref matchScore);
                        if (matchScore >= this._Instruction.MatchScoreThreshold && rect != RectangleF.Empty)
                        {
                            pageNumbers.Add(pageNum);
                            if (PdfImageReader.GetPageImage(filePath, pageNum) != null)
                            {
                                scanResult.IsScanned = true;
                            }
                            else
                            {
                                scanResult.IsElectronic = true;
                            }
                        }
                    }
                    catch (Exception ex2)
                    {
                        if(this.WorkerErredEvent !=null)
                        {
                            this.WorkerErredEvent(this._JobTicketID, this.GetType(), this._Instruction, ex2.ToString(),true);
                        }
                    }
                }
                scanResult.PageNumbers = null;
                if (pageNumbers.Count > 0)
                {
                    lock (_SyncObj)
                    {
                        scanResult.PageNumbers = pageNumbers.ToArray();
                        string fileType = "Unknown";
                        if (scanResult.IsElectronic)
                            fileType = "electronic";
                        else if(scanResult.IsScanned)
                        {
                            fileType = "scanned";
                        }
                        
                        this._OutputWriter.WriteStartElement("File");
                        this._OutputWriter.WriteAttributeString("DocID",this._FilePathIDMappings[filePath].ToString());
                        this._OutputWriter.WriteAttributeString("Pages",IntUtil.ToString(scanResult.PageNumbers));
                        this._OutputWriter.WriteAttributeString("IsElectronic",scanResult.IsElectronic.ToString());
                        this._OutputWriter.WriteAttributeString("IsScanned",scanResult.IsScanned.ToString());
                        this._OutputWriter.WriteEndElement();
                        this._OutputWriter.Flush();

                        this._LogWriter.WriteLine(
                            string.Format("{0}: {1} - {2} ({3})",
                                          DateTime.Now.ToString(),
                                          fileName,
                                          IntUtil.ToString(scanResult.PageNumbers),
                                          fileType));
                        this._LogWriter.Flush();
                    }
                }
                else
                {
                    lock (_SyncObj)
                    {
                        this._Output.CorruptedFiles.Add(filePath);
                        this._LogWriter.WriteLine(string.Format("{0}: {1} - {2}", DateTime.Now.ToString(), fileName,
                                                          "unable to find pattern"));
                        this._LogWriter.Flush();
                    }
                }
            }
            catch (Exception ex1)
            {
                lock(_SyncObj)
                {
                    this._Output.CorruptedFiles.Add(filePath);
                    this._ErrorWriter.WriteLine(string.Format("{0}: {1} - error: {2}", DateTime.Now.ToString(),
                                                      fileName, ex1.ToString()));
                    this._ErrorWriter.Flush();
                }
            }
            finally
            {
                this._ManualResets[idx].Set();
            }
        }

        private Dictionary<string,int> ReadFileList(string fileListXmlFilePath)
        {
            Dictionary<string,int> filePathIDMappings = new Dictionary<string, int>();
            XmlDocument xDoc=new XmlDocument();
            xDoc.Load(fileListXmlFilePath);
            XmlNodeList fileNodes = xDoc.SelectNodes("//Files/File");
            if(fileNodes !=null && fileNodes.Count>0)
            {
                foreach(XmlNode fileNode in fileNodes)
                {
                    int docID = int.Parse(fileNode.Attributes.GetNamedItem("DocID").Value);
                    string filePath = fileNode.Attributes.GetNamedItem("FilePath").Value;
                    filePathIDMappings.Add(filePath, docID);
                }
            }
            return filePathIDMappings;
        }
        #endregion
    }
}
